// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.ui;

import bats.Bats;
import bats.SpatialProperties;
import bats.map.MapNode;
import bats.scene.IndicatorsNode;
import bats.scene.Lighting;
import bats.scene.LightingPreset;
import bats.scene.PickablesNode;
import bats.scene.PointOfView;
import bats.scene.PovMode;
import bats.scene.SceneNode;
import bats.scene.WorldNode;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import de.lessvoid.nifty.NiftyEventSubscriber;
import de.lessvoid.nifty.controls.CheckBoxStateChangedEvent;
import de.lessvoid.nifty.controls.RadioButtonStateChangedEvent;
import de.lessvoid.nifty.controls.SliderChangedEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MyCamera;
import jme3utilities.MyString;
import jme3utilities.debug.SkeletonDebugControl;
import jme3utilities.math.MyMath;
import jme3utilities.nifty.GuiScreenController;
import jme3utilities.ui.InputMode;

/**
 * A GUI screen controller for the heads-up display (HUD) of a particular input
 * mode. Use setEnabled() to reveal or hide the display.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class HeadsUpDisplay
        extends GuiScreenController {
    // *************************************************************************
    // constants

    /**
     * the number of seconds in a minute
     */
    final private static int secondsPerMinute = 60;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(HeadsUpDisplay.class.getName());
    /**
     * items in the "add block" popup menu
     */
    final static String addBlockItems[] = {
        "arcade", "corridor", "corridorPavement", "gateway",
        "ground", "ground2x2", "ground4x4",
        "ladder", "merlonLeft", "merlonRight",
        "parapet",
        "pavement", "plain", "plain8x8",
        "platform", "pond", "pool",
        "stairwayLeft", "stairwayRight", "tower"
    };
    /**
     * items in the "add prop" popup menu
     */
    final static String addPropItems[] = {
        "barrel", "hogshead", "mantlet", "tun"
    };
    /**
     * items in the "add unit" popup menu, including optional ones
     */
    final private static String addUnitItemsAll[] = {
        "sinbad", "oto", "bob", "nan",
        "bigOto", "bigBob", "miniBob"
    };
    /**
     * items in the "add unit" popup menu in basic mode
     */
    final private static String addUnitItemsBasic[] = {
        "oto", "bob", "bigOto", "bigBob"
    };
    /**
     * items in the "load map" popup menu
     */
    final static String loadMapItems[] = {
        "castle", "pond", "test", "user"
    };
    /**
     * Nifty element id of the axes checkbox
     */
    final static String axesCheckBoxId = "axesCheckBox";
    /**
     * Nifty element id of the 3D cursor checkbox
     */
    final static String cursor3DCheckBoxId = "3DCursorCheckBox";
    /**
     * Nifty element id of the ground checkbox
     */
    final static String groundCheckBoxId = "groundCheckBox";
    /**
     * Nifty element id of the pause checkbox
     */
    final static String pauseCheckBoxId = "pauseCheckBox";
    /**
     * Nifty element id of the physics debug checkbox
     */
    final static String physicsDebugCheckBoxId = "physicsDebugCheckBox";
    /**
     * Nifty element id of the pick indicator checkbox
     */
    final static String pickIndicatorCheckBoxId = "pickIndicatorCheckBox";
    /**
     * Nifty element id of the shadows checkbox
     */
    final static String shadowsCheckBoxId = "shadowsCheckBox";
    /**
     * Nifty element id of the skeleton debug checkbox
     */
    final static String skeletonDebugCheckBoxId = "skeletonDebugCheckBox";
    /**
     * Nifty element id of the targets debug checkbox
     */
    final static String targetsDebugCheckBoxId = "targetsDebugCheckBox";
    /**
     * Nifty element id of the terrain checkbox
     */
    final static String terrainCheckBoxId = "terrainCheckBox";
    /**
     * Nifty element id of the viewpoints debug checkbox
     */
    final static String viewpointDebugCheckBoxId = "viewpointDebugCheckBox";
    // *************************************************************************
    // fields
    /**
     * scene controlled by this display: set by constructor
     */
    final protected SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate a disabled display for the specified input mode and scene.
     * After instantiation, the first method invoked should be initialize().
     *
     * @param mode which input mode (disabled, not null)
     * @param scene (not null)
     * @param id Nifty screen id (not null)
     */
    HeadsUpDisplay(InputMode mode, SceneNode scene, String id) {
        super(id, String.format("Interface/Nifty/huds/%s.xml", id), false);

        if (mode == null) {
            throw new NullPointerException("mode should not be null");
        }
        if (mode.isEnabled()) {
            throw new IllegalArgumentException("mode should not be enabled");
        }
        if (scene == null) {
            throw new NullPointerException("scene should not be null");
        }

        setListener(mode);
        this.scene = scene;

        assert !isEnabled();
        assert !isInitialized();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Copy the items in the "add unit" menu.
     *
     * @return a new array
     */
    public static String[] getAddUnitItems() {
        if (Bats.getParameters().allUnits()) {
            return addUnitItemsAll.clone();
        } else {
            return addUnitItemsBasic.clone();
        }
    }

    /**
     * Access the display of the enabled game mode, if any.
     *
     * @return the pre-existing instance (or null if none)
     */
    public static HeadsUpDisplay getCurrent() {
        GameMode mode = GameMode.getEnabledMode();
        if (mode != null) {
            HeadsUpDisplay hud = mode.getHud();
            return hud;
        } else {
            return null;
        }
    }

    /**
     * Callback which Nifty invokes after the user toggles the state of a
     * checkbox.
     *
     * @param checkBoxId Nifty element id of the checkbox (not null)
     * @param event details of the event (not null)
     */
    @NiftyEventSubscriber(pattern = ".*CheckBox")
    public void onCheckBoxChanged(final String checkBoxId,
            final CheckBoxStateChangedEvent event) {
        assert checkBoxId != null;
        if (!hasStarted()) {
            return;
        }

        boolean checked = event.isChecked();
        WorldNode world = scene.getWorld();
        Spatial model = world.getPickables().getOnlyChild();
        switch (checkBoxId) {
            case axesCheckBoxId:
                scene.getIndicators().getAxes().setEnabled(checked);
                return;

            case cursor3DCheckBoxId:
                scene.getIndicators().setCursor(checked);
                return;

            case groundCheckBoxId:
                world.getMap().setGround(checked);
                return;

            case pauseCheckBoxId:
                scene.setEnabled(!checked);
                return;

            case physicsDebugCheckBoxId:
                world.getPhysics().setDebugging(checked);
                return;

            case pickIndicatorCheckBoxId:
                if (model != null) {
                    SpatialProperties.setPicked(model, checked);
                }
                return;

            case shadowsCheckBoxId:
                scene.getLighting().setShadowsEnabled(checked);
                return;

            case skeletonDebugCheckBoxId:
                SkeletonDebugControl.setDebugEnabled(model, checked);
                return;

            case targetsDebugCheckBoxId:
                world.setTargetsAll(checked);
                return;

            case terrainCheckBoxId:
                world.getMap().setTerrain(checked);
                return;

            case viewpointDebugCheckBoxId:
                world.setViewpointAll(checked);
                return;
        }
        logger.log(Level.WARNING, "unknown checkbox: id={0}",
                MyString.quote(checkBoxId));
    }

    /**
     * Callback which Nifty invokes after the user selects a radio button.
     *
     * @param buttonId Nifty element id of the radio button (not null)
     * @param event details of the event (not null)
     */
    @NiftyEventSubscriber(pattern = ".*RadioButton")
    public void onRadioButtonChanged(final String buttonId,
            final RadioButtonStateChangedEvent event) {
        assert buttonId != null;
        if (!hasStarted()) {
            return;
        }

        PointOfView pov = scene.getPov();
        switch (buttonId) {
            case "firstPersonRadioButton":
                if (pov.canGoFirstPerson()) {
                    pov.setMode(PovMode.FIRST_PERSON);
                }
                return;

            case "flyingRadioButton":
                pov.setMode(PovMode.FLYING);
                return;

            case "orbitingRadioButton":
                pov.setMode(PovMode.ORBITING);
                return;
        }
        logger.log(Level.WARNING, "unknown radio button: id={0}",
                MyString.quote(buttonId));
    }

    /**
     * Callback which Nifty invokes after the user moves a slider.
     *
     * @param sliderId Nifty element id of the slider (not null)
     * @param event details of the event (not null)
     */
    @NiftyEventSubscriber(pattern = ".*Slider")
    public void onSliderChanged(final String sliderId,
            final SliderChangedEvent event) {
        assert sliderId != null;
        if (!hasStarted()) {
            return;
        }

        float value = event.getValue();
        switch (sliderId) {
            case "fovSlider":
                MyCamera.setYTangent(cam, value);
                return;
        }
        logger.log(Level.WARNING, "unknown slider: id={0}",
                MyString.quote(sliderId));
    }

    /**
     * Process an action which might affect this display.
     *
     * @param words the action string split into an array of words (not null)
     * @return true if the action was handled, false if it wasn't handled
     */
    boolean processAction(String[] words) {
        switch (words[0]) {
            case "add":
                if (words.length == 2) {
                    switch (words[1]) {
                        case "block":
                            showPopup(words, addBlockItems);
                            return true;
                        case "prop":
                            showPopup(words, addPropItems);
                            return true;
                        case "unit":
                            showPopup(words, getAddUnitItems());
                            return true;
                    }
                }
                break;

            case "load":
                if (words.length == 2 && "map".equals(words[1])) {
                    showPopup(words, loadMapItems);
                    return true;
                }
                break;

            case "select":
                if (words.length == 2) {
                    switch (words[1]) {
                        case "lighting":
                            showPopup(words, LightingPreset.selectLightingItems);
                            return true;
                        case "pov":
                            showPopup(words,
                                    scene.getPov().getSelectPovItems());
                            return true;
                    }
                }
                break;

            case "toggle":
                if (words.length == 2 && "hud".equals(words[1])) {
                    toggleVisible();
                    return true;
                }
                break;
        }
        /*
         * The action has not been handled yet.
         */
        return false;
    }
    // *************************************************************************
    // new protected methods

    /**
     * Update the user's view of this display.
     */
    protected void updateView() {
        logger.log(Level.FINE, "{0}", scene);

        updateCheckBoxes();
        updateMapButtonLabels();
        updatePickableButtonLabels();
        updatePov();
        updateSimTime();
        /*
         * picked spatials
         */
        PickablesNode pickables = scene.getWorld().getPickables();
        String description = pickables.describePick();
        setStatusText("pickStatus", description);
        /*
         * lights preset
         */
        Lighting lights = scene.getLighting();
        String presetsStatus = lights.getPreset().describe();
        setStatusText("lightsPresetStatus", presetsStatus);
    }
    // *************************************************************************
    // AbstractAppState methods

    /**
     * Update this display. (Invoked once per frame.)
     *
     * @param simInterval seconds since previous update (&ge;0)
     */
    @Override
    public void update(float simInterval) {
        assert simInterval >= 0f : simInterval;
        assert isEnabled();
        super.update(simInterval);

        updateView();
    }
    // *************************************************************************
    // private methods

    /**
     * Describe the POV.
     *
     * @return a brief description (not null)
     */
    private String describePov(PovMode mode) {
        PointOfView pov = scene.getPov();
        Spatial subject = pov.getSubject();
        switch (mode) {
            case ORBITING:
                return "orbiting " + subject.getName();
            case FLYING:
                return "flying ghost";
            case FIRST_PERSON:
                if (!SpatialProperties.isUnit(subject)) {
                    subject = scene.getWorld().getPickables().getOnlyChild();
                }
                return subject.getName();
        }
        throw new IllegalStateException();
    }

    /**
     * Toggle the visibility of this display.
     */
    private void toggleVisible() {
        boolean newState = !isEnabled();
        setEnabled(newState);
    }

    /**
     * Update all check boxes in the HUD.
     */
    private void updateCheckBoxes() {
        IndicatorsNode indicators = scene.getIndicators();
        WorldNode world = scene.getWorld();

        boolean checked = indicators.getAxes().isEnabled();
        setCheckBox(axesCheckBoxId, checked);

        checked = indicators.getCursor3D().isEnabled();
        setCheckBox(cursor3DCheckBoxId, checked);

        checked = world.getMap().hasGround();
        setCheckBox(groundCheckBoxId, checked);

        checked = !scene.isEnabled();
        setCheckBox(pauseCheckBoxId, checked);

        checked = world.getPhysics().isDebugging();
        setCheckBox(physicsDebugCheckBoxId, checked);

        checked = world.getPickables().countPicked() > 0;
        setCheckBox(pickIndicatorCheckBoxId, checked);

        checked = scene.getLighting().areShadowsEnabled();
        setCheckBox(shadowsCheckBoxId, checked);

        checked = world.getMap().hasTerrain();
        setCheckBox(terrainCheckBoxId, checked);

        boolean skeletonDebug = false;
        boolean targetsDebug = false;
        boolean viewpointDebug = false;
        Spatial model = scene.getWorld().getPickables().getOnlyChild();
        if (model != null && SpatialProperties.isUnit(model)) {
            skeletonDebug = SkeletonDebugControl.isDebugEnabled(model);
            targetsDebug = SpatialProperties.areTargetsVisible(model);
            viewpointDebug = SpatialProperties.isViewpointVisible(model);
        }
        setCheckBox(skeletonDebugCheckBoxId, skeletonDebug);
        setCheckBox(targetsDebugCheckBoxId, targetsDebug);
        setCheckBox(viewpointDebugCheckBoxId, viewpointDebug);
    }

    /**
     * Update the labels of this display's world map buttons.
     */
    private void updateMapButtonLabels() {
        MapNode worldMap = scene.getWorld().getMap();

        String lockLabel = "";
        String unlockLabel = "";
        if (worldMap.hasBlock("gateway")) {
            lockLabel = "Lock all";
            unlockLabel = "Unlock all";
        }
        setButtonLabel("lockButton", lockLabel);
        setButtonLabel("unlockButton", unlockLabel);
    }

    /**
     * Update the labels of the pickable buttons.
     */
    private void updatePickableButtonLabels() {
        String aggroLabel = "";
        String clearLabel = "";
        String deleteLabel = "";
        String duplicateLabel = "";
        String impairLabel = "";
        String reloadLabel = "";
        String similarLabel = "";

        PickablesNode pickables = scene.getWorld().getPickables();
        int count = pickables.countPicked();
        if (count > 0) {
            aggroLabel = "Toggle aggro";
            clearLabel = "Clear the pick";
            deleteLabel = "Delete picked";
            duplicateLabel = "Duplicate picked";
            impairLabel = "Impair picked";
            reloadLabel = "Reload picked";
            similarLabel = "Pick similar";
        }
        setButtonLabel("aggroButton", aggroLabel);
        setButtonLabel("clearButton", clearLabel);
        setButtonLabel("deleteButton", deleteLabel);
        setButtonLabel("duplicateButton", duplicateLabel);
        setButtonLabel("impairButton", impairLabel);
        setButtonLabel("reloadButton", reloadLabel);
        setButtonLabel("similarButton", similarLabel);

        String allLabel = "";
        int max = pickables.countChildren();
        if (count < max) {
            allLabel = "Pick all";
        }
        setButtonLabel("allButton", allLabel);

        String invertLabel = "";
        if (max > 0) {
            invertLabel = "Invert the pick";
        }
        setButtonLabel("invertButton", invertLabel);
    }

    /**
     * Update the point of view status.
     */
    private void updatePov() {
        PointOfView pov = scene.getPov();
        boolean canGoFirstPerson = pov.canGoFirstPerson();
        setRadioButtonEnabled("firstPersonRadioButton", canGoFirstPerson);
        PovMode povMode = pov.getMode();
        setRadioButton(povMode.buttonId());
        String modeLabel = "";
        if (canGoFirstPerson) {
            modeLabel = describePov(PovMode.FIRST_PERSON);
        }
        setStatusText(PovMode.FIRST_PERSON.labelId(), modeLabel);
        modeLabel = describePov(PovMode.FLYING);
        setStatusText(PovMode.FLYING.labelId(), modeLabel);
        modeLabel = describePov(PovMode.ORBITING);
        setStatusText(PovMode.ORBITING.labelId(), modeLabel);

        float azimuthDegrees = MyCamera.azimuth(cam) * FastMath.RAD_TO_DEG;
        azimuthDegrees = MyMath.modulo(azimuthDegrees, 360f);
        String povStatus2 = String.format("view azimuth = %03.0f",
                azimuthDegrees);
        setStatusText("povStatus2", povStatus2);

        float fovYDegrees = MyCamera.fovY(cam) * FastMath.RAD_TO_DEG;
        String fovStatus = String.format("field of view = %.0f", fovYDegrees);
        setStatusText("fovStatus", fovStatus);
        float yTangent = MyCamera.yTangent(cam);
        setSlider("fov", yTangent);

        String povStatus3 = "";
        if (povMode == PovMode.ORBITING) {
            float range = pov.getRange();
            povStatus3 = String.format("range = %.1f meters", range);
        } else {
            MapNode map = scene.getWorld().getMap();
            Vector3f location = pov.getLocation();
            Float altitude = map.measureDrop(location, 0.01f);
            if (altitude != null) {
                povStatus3 = String.format("%.1f meters above map", altitude);
            }
        }
        setStatusText("povStatus3", povStatus3);
    }

    /**
     * Update the simulation time status.
     */
    private void updateSimTime() {
        float simTime = scene.getSimTime();
        float seconds = simTime % secondsPerMinute;
        float minutes = (simTime - seconds) / secondsPerMinute;
        String simTimeText;
        if (minutes > 0) {
            /*
             * Display minutes and seconds after the first minute.
             */
            simTimeText = String.format("%.0f:%02.0f", minutes, seconds);
        } else {
            /*
             * Display tenths of seconds during the first minute.
             */
            simTimeText = String.format("%4.1f", simTime);
        }
        setStatusText("simTimeStatus", simTimeText);
    }
}